﻿using Nemerle;
using Nemerle.Collections;
using Nemerle.Compiler;
using Nemerle.Text;
using Nemerle.Utility;
using Nemerle.Compiler.Parsetree;

using System;
using System.Collections.Generic;
using System.Linq;

using NRails.Database;
using NRails.Database.Schema;

namespace NRails.Macros.Model
{
  internal module DatabaseManagerImpl
  {
    [Record]
    class MyTypeInfo: IComparable[MyTypeInfo]
    {
      [Accessor] name : string; 
      [Accessor] fullName : string; 
      
      public CompareTo(obj : object) : int
      {
          def other = obj :> MyTypeInfo;
          def cmp = Comparer.[string].Default;
          
          match (cmp.Compare(name, other.Name))
          {
             | 0 => cmp.Compare(fullName, other.fullName);
             | x => x
          }
      }
    }

    private GetModelTypes(t : TypeBuilder) : Set[MyTypeInfo]
    {
      if (t.Manager.UserData.Contains("ModelTypes"))
        t.Manager.UserData["ModelTypes"] :> Set[MyTypeInfo]
      else
        Set.[MyTypeInfo]()
    }
    
    public GetDbType(manager: ManagerClass) : TypeBuilder
    {
        manager.UserData["_NRailsDbType"] :> TypeBuilder
    }
    
    public SetDbType(t : TypeBuilder) : void
    {
        t.Manager.UserData["_NRailsDbType"] = t;
    }
    
    private SetModelTypes(t : TypeBuilder, modelTypes : Set[MyTypeInfo]) : void
    {
        t.Manager.UserData["ModelTypes"] = modelTypes;
    }

    public GetTypeForTable(t : TypeBuilder, name : string) : string
    {
      GetModelTypes(t).Where(t => t.Name == name).Select(t => t.FullName).SingleOrDefault()
    }

    public RegisterModelType(t : TypeBuilder, name : string, fullName : string) : void
    {
      def info = MyTypeInfo(name, fullName);
      def modelTypes = GetModelTypes(t);
      if (modelTypes.Any(t => t.Name == name))
        Message.Warning($"Type $fullName already registered in DatabaseManager");
      else
        SetModelTypes(t, modelTypes.Add(info));
    }
    
    public GenerateHelpers(t : TypeBuilder) : void
    {
      t.Define(<[decl: static engine : NRails.Engine; ]>);
      t.Define(<[decl: public Engine : NRails.Engine { get {engine} }; ]>);
      t.Define(<[decl: static env : string; ]>);
      t.Define(<[decl: public Env : string { get {env} }; ]>);
      t.Define(<[decl:
        static this()
        {
          engine = Engine.Instance;
          env = engine.Cfg.Env;
        }
      ]>);
      t.Define(<[decl:
        public this()
        {
          base(engine.Cfg.ConnectionStringName);
        }
      ]>);
    }
    
    public GenerateTables(t : TypeBuilder) : void
    {
      def existingProperties = t.GetProperties();
      def existingMethods = t.GetMethods();
      def hintBuilder = Text.StringBuilder();
      def modelTypes = GetModelTypes(t);
      def engine = t.Manager.GetNRailsEngine();
      
      foreach (modelType in modelTypes)
      {
        def typeName = modelType.Name;
        def propertyName = $"$(typeName)s";
        def typeFullName = modelType.FullName;
        def modelType = PExpr.FromQualifiedIdentifier(t.Manager, typeFullName);

        unless (existingProperties.Any(p => p.Name == propertyName))
        {
            
            t.Define(<[decl:
                public $(propertyName : dyn) : BLToolkit.Data.Linq.Table[ $modelType ]
                {
                  get { this.GetTable.[$modelType](); }
                }
            ]>);

            _ = hintBuilder.Append($"$propertyName : table of $(typeName)\n");
        }

        def table = engine.GetTable(typeName);
        def pk = table.Keys
            .Where(k => k.KeyType == ConstraintType.KeyPrimary)
            .Select(k => k.Columns.Split(',').ToNList().Map(_.Trim()))
            .SingleOrDefault();
        
        def getByKeyName = $"$(typeName)ByKey";
        
        when (pk != null && pk.Any() && !existingMethods.Any(p => p.Name == getByKeyName))
        {
            def cols = pk.Map(table.GetColumn(_));
            def parms = cols.Map(col => {
                def parType = ModelImpl.ConvertToNemerleType(t.Manager, col);
                
                PParameter(<[ $(col.Name : dyn) : $parType ]>)
            });
            
            def model = <[ $("model" : usesite) ]>;
            
            def parmsAndCols = parms.Zip(cols);
            
            def makeAnds(parmsAndCols)
            {
                | (parm, col) :: [] =>
                    <[ $model.$(col.Name : dyn) == $(parm.Name : dyn) ]>
                | (parm, col) :: tail => 
                    <[ $model.$(col.Name : dyn) == $(parm.Name : dyn) && $(makeAnds(tail)) ]>
            }
            
            
            t.Define(<[decl:
                public $(getByKeyName : dyn)(..$parms) : $modelType 
                {
                    this.GetTable.[$modelType]().Single($model => { 
                        $(makeAnds(parmsAndCols))
                    });
                }
            ]>);
        }
      }

      unless (hintBuilder.Length == 0)
          Message.Hint("\n" + hintBuilder.ToString());
    }
  }
}
